@@ -1,6 +1,7 @@ |
||
| 1 | 1 |
source 'https://rubygems.org' |
| 2 | 2 |
|
| 3 | 3 |
gem 'rails' |
| 4 |
+gem 'rake' |
|
| 4 | 5 |
gem 'mysql2' |
| 5 | 6 |
gem 'devise' |
| 6 | 7 |
gem 'rails_admin' |
@@ -0,0 +1,39 @@ |
||
| 1 |
+# This controller is designed to allow your Agents to receive cross-site Webhooks (posts). When POSTed, your Agent will |
|
| 2 |
+# have #receive_webhook called on itself with the POST params. |
|
| 3 |
+# |
|
| 4 |
+# Make POSTs to the following URL: |
|
| 5 |
+# http://yourserver.com/users/:user_id/webhooks/:agent_id/:secret |
|
| 6 |
+# where :user_id is your User's id, :agent_id is an Agent's id, and :secret is a token that should be |
|
| 7 |
+# user-specifiable in your Agent. It is highly recommended that you verify this token whenever #receive_webhook |
|
| 8 |
+# is called. For example, one of your Agent's options could be :secret and you could compare this value |
|
| 9 |
+# to params[:secret] whenever #receive_webhook is called on your Agent, rejecting invalid requests. |
|
| 10 |
+# |
|
| 11 |
+# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code]. For example: |
|
| 12 |
+# [{status: "success"}, 200]
|
|
| 13 |
+# or |
|
| 14 |
+# ["not found", 404] |
|
| 15 |
+ |
|
| 16 |
+class WebhooksController < ApplicationController |
|
| 17 |
+ skip_before_filter :authenticate_user! |
|
| 18 |
+ |
|
| 19 |
+ def create |
|
| 20 |
+ user = User.find_by_id(params[:user_id]) |
|
| 21 |
+ if user |
|
| 22 |
+ agent = user.agents.find_by_id(params[:agent_id]) |
|
| 23 |
+ if agent |
|
| 24 |
+ response, status = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id)) |
|
| 25 |
+ if response.is_a?(String) |
|
| 26 |
+ render :text => response, :status => status || 200 |
|
| 27 |
+ elsif response.is_a?(Hash) |
|
| 28 |
+ render :json => response, :status => status || 200 |
|
| 29 |
+ else |
|
| 30 |
+ head :ok |
|
| 31 |
+ end |
|
| 32 |
+ else |
|
| 33 |
+ render :text => "agent not found", :status => :not_found |
|
| 34 |
+ end |
|
| 35 |
+ else |
|
| 36 |
+ render :text => "user not found", :status => :not_found |
|
| 37 |
+ end |
|
| 38 |
+ end |
|
| 39 |
+end |
@@ -60,6 +60,11 @@ class Agent < ActiveRecord::Base |
||
| 60 | 60 |
# Implement me in your subclass of Agent. |
| 61 | 61 |
end |
| 62 | 62 |
|
| 63 |
+ def receive_webhook(params) |
|
| 64 |
+ # Implement me in your subclass of Agent. |
|
| 65 |
+ ["not implemented", 404] |
|
| 66 |
+ end |
|
| 67 |
+ |
|
| 63 | 68 |
# Implement me in your subclass to decide if your Agent is working. |
| 64 | 69 |
def working? |
| 65 | 70 |
raise "Implement me in your subclass" |
@@ -88,6 +93,13 @@ class Agent < ActiveRecord::Base |
||
| 88 | 93 |
message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" }
|
| 89 | 94 |
end |
| 90 | 95 |
|
| 96 |
+ def trigger_webhook(params) |
|
| 97 |
+ receive_webhook(params).tap do |
|
| 98 |
+ self.last_webhook_at = Time.now |
|
| 99 |
+ save! |
|
| 100 |
+ end |
|
| 101 |
+ end |
|
| 102 |
+ |
|
| 91 | 103 |
def set_default_schedule |
| 92 | 104 |
self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
| 93 | 105 |
end |
@@ -16,6 +16,7 @@ Huginn::Application.routes.draw do |
||
| 16 | 16 |
match "/worker_status" => "worker_status#show" |
| 17 | 17 |
|
| 18 | 18 |
post "/users/:user_id/update_location/:secret" => "user_location_updates#create" |
| 19 |
+ post "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#create" |
|
| 19 | 20 |
|
| 20 | 21 |
mount RailsAdmin::Engine => '/admin', :as => 'rails_admin' |
| 21 | 22 |
# match "/delayed_job" => DelayedJobWeb, :anchor => false |
@@ -0,0 +1,5 @@ |
||
| 1 |
+class AddLastWebhookAtToAgents < ActiveRecord::Migration |
|
| 2 |
+ def change |
|
| 3 |
+ add_column :agents, :last_webhook_at, :datetime |
|
| 4 |
+ end |
|
| 5 |
+end |
@@ -11,7 +11,7 @@ |
||
| 11 | 11 |
# |
| 12 | 12 |
# It's strongly recommended to check this file into your version control system. |
| 13 | 13 |
|
| 14 |
-ActiveRecord::Schema.define(:version => 20130126080736) do |
|
| 14 |
+ActiveRecord::Schema.define(:version => 20130509053743) do |
|
| 15 | 15 |
|
| 16 | 16 |
create_table "agents", :force => true do |t| |
| 17 | 17 |
t.integer "user_id" |
@@ -26,6 +26,7 @@ ActiveRecord::Schema.define(:version => 20130126080736) do |
||
| 26 | 26 |
t.datetime "created_at", :null => false |
| 27 | 27 |
t.datetime "updated_at", :null => false |
| 28 | 28 |
t.text "memory", :limit => 2147483647 |
| 29 |
+ t.datetime "last_webhook_at" |
|
| 29 | 30 |
end |
| 30 | 31 |
|
| 31 | 32 |
add_index "agents", ["schedule"], :name => "index_agents_on_schedule" |
@@ -0,0 +1,54 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe WebhooksController do |
|
| 4 |
+ class Agents::WebhookReceiverAgent < Agent |
|
| 5 |
+ cannot_receive_events! |
|
| 6 |
+ cannot_be_scheduled! |
|
| 7 |
+ |
|
| 8 |
+ def receive_webhook(params) |
|
| 9 |
+ if params.delete(:secret) == options[:secret] |
|
| 10 |
+ memory[:webhook_values] = params |
|
| 11 |
+ ["success", 200] |
|
| 12 |
+ else |
|
| 13 |
+ ["failure", 404] |
|
| 14 |
+ end |
|
| 15 |
+ end |
|
| 16 |
+ end |
|
| 17 |
+ |
|
| 18 |
+ before do |
|
| 19 |
+ stub(Agents::WebhookReceiverAgent).valid_type?("Agents::WebhookReceiverAgent") { true }
|
|
| 20 |
+ @agent = Agents::WebhookReceiverAgent.new(:name => "something", :options => { :secret => "my_secret" })
|
|
| 21 |
+ @agent.user = users(:bob) |
|
| 22 |
+ @agent.save! |
|
| 23 |
+ end |
|
| 24 |
+ |
|
| 25 |
+ it "should not require login to trigger a webhook" do |
|
| 26 |
+ @agent.last_webhook_at.should be_nil |
|
| 27 |
+ post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
| 28 |
+ @agent.reload.last_webhook_at.should be_within(2).of(Time.now) |
|
| 29 |
+ response.body.should == "success" |
|
| 30 |
+ response.should be_success |
|
| 31 |
+ end |
|
| 32 |
+ |
|
| 33 |
+ it "should call receive_webhook" do |
|
| 34 |
+ post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5" |
|
| 35 |
+ @agent.reload.memory[:webhook_values].should == { :key => "value", :another_key => "5" }
|
|
| 36 |
+ response.body.should == "success" |
|
| 37 |
+ response.should be_success |
|
| 38 |
+ |
|
| 39 |
+ post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go" |
|
| 40 |
+ @agent.reload.memory[:webhook_values].should_not == { :no => "go" }
|
|
| 41 |
+ response.body.should == "failure" |
|
| 42 |
+ response.should be_missing |
|
| 43 |
+ end |
|
| 44 |
+ |
|
| 45 |
+ it "should fail on incorrect users" do |
|
| 46 |
+ post :create, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go" |
|
| 47 |
+ response.should be_missing |
|
| 48 |
+ end |
|
| 49 |
+ |
|
| 50 |
+ it "should fail on incorrect agents" do |
|
| 51 |
+ post :create, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go" |
|
| 52 |
+ response.should be_missing |
|
| 53 |
+ end |
|
| 54 |
+end |
@@ -49,7 +49,7 @@ describe Agent do |
||
| 49 | 49 |
end |
| 50 | 50 |
end |
| 51 | 51 |
|
| 52 |
- describe "with a mock source" do |
|
| 52 |
+ describe "with an example Agent" do |
|
| 53 | 53 |
class Agents::SomethingSource < Agent |
| 54 | 54 |
default_schedule "2pm" |
| 55 | 55 |
|